package com.jdbc.cms.utils;

import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

/**
 * Copyright (c) 2023-2033 All Rights Reserved
 *
 * JDBC Part6 封装 DAO 层数据库操作
 * TODO:
 *  1. DAO层执行数据库操作，分为两种场景：非DQL语句、DQL语句
 *  2. 两种查询语句分别对外提供两种执行方法，executeUpdate()、executeQuery()
 *  3. 方法入参为变长参数，Object... 用于应对不同个数占位符的赋值，可以是 0~n 个
 *  4. 变长参数的遍历，变长参数作为数组使用，通过 for 循环遍历
 *  5. 技巧：生成for循环的快捷方式 param.length.fori
 *  6. 注意 preparedStatement 占位符赋值时下标从1开始
 *
 * @date: 2023/9/25
 */
public class BaseDao {

    /**
     * 执行非DQL语句
     * TODO: 通过工具类获取连接、创建对象、占位符赋值、关闭连接
     * @param sql       带占位符的SQL
     * @param params    占位符的值，变量个数动态变化，用变长参数替换 List<Object>，
     * @return  int     执行的行数
     */
    public int executeUpdate(String sql, Object... params) throws SQLException {
        // 1. 通过工具类获取连接
        Connection connection = JdbcUtils.getConnection();

        // 2. 创建PreparedStatement对象
        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        // 3. 遍历变长参数，注意数据库的下标从1开始
        /**
         * TODO: 变长参数作为数组使用，注意占位符赋值时下标从1开始
         */
        for (int i = 0; i < params.length; i++) {
            preparedStatement.setObject(i+1, params[i]);
        }

        // 4. 执行SQL
        int rows = preparedStatement.executeUpdate();

        // 5. 关闭连接
        preparedStatement.close();
        /**
         * 需要考虑是否开启了事务
         * 1. 如果开启了事务，肯定关闭了自动提交，由业务层Service统一处理连接，DAO层忽视
         * 2. 如果没有开启事务，自动提交仍为true，则直接调用工具类释放连接。
         */
        if (connection.getAutoCommit()) {
            // 没有开启事务，调用工具类释放连接
            JdbcUtils.freeConnection();
        }

        return rows;
    }


    /**
     * 执行DQL语句
     * TODO: 查询结果映射
     *  1. 数据库的一行数据，对应，一个Java类实例对象
     *  2. 数据库的多行数据，对应，一个实体类集合
     *  3. 将查询结果保存到泛型对象中 T，实现数据--》实体类映射转换
     *  4. 泛型，声明方法时，在返回值前添加```<T>```表示当前函数使用了泛型变量，不确定类型。在调用方法时，要在方法前显示指定变量类型<User>
     *  5. 反射，用反射对类的属性赋值
     *
     * @param clazz     接收数据转换后的实体类模板对象，查询员工信息，clazz传入Employee.class，部门信息传入Department.class
     *                  TODO：作用：1. 确定泛型 2. 用反射对类的属性赋值
     * @param sql       查询语句，要求字段名和实体类的属性名相同
     * @param params    占位符的值
     * @return List<T>  实体类的集合
     * @param <T>       声明的结果的类型，作用 1. 确定泛型，当传入参数为 User.Class 时，T=User 2. 用反射进行赋值
     * @throws SQLException
     * @throws InstantiationException
     * @throws IllegalAccessException
     * @throws NoSuchFieldException
     */
    public <T> List<T> executeQuery(Class<T> clazz, String sql, Object... params) throws SQLException,
            InstantiationException, IllegalAccessException, NoSuchFieldException {
        // 1. 通过工具类创建连接
        Connection connection = JdbcUtils.getConnection();
        // 2. 创建PreparedStatement
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        // 3. 对占位符的入参做校验后按顺序赋值，注意数据库数据下标从1开始
        if (params != null && params.length != 0) {
            for (int i = 0; i < params.length; i++) {
                preparedStatement.setObject(i+1, params[i]);
            }
        }
        // 4. 执行SQL，获取查询的结果集
        ResultSet resultSet = preparedStatement.executeQuery();

        // 5. 结果集分析
        // TODO：1. 将结果集数据转为T对象数组
        List<T> mapList = new ArrayList<>();

        // 获取结果字段信息集ResultSetMetaData
        ResultSetMetaData metaData = resultSet.getMetaData();
        // 获取字段个数
        int columnCount = metaData.getColumnCount();

        // TODO: 2. 按行遍历结果集，取出行数据
        while (resultSet.next()) {
            // TODO： 3. 调用类的无参构造函数实例化对象，对应一行数据
            T t = clazz.newInstance();
            // TODO： 4. 将当前行的字段数据，赋值到，泛型对象的属性
            for (int i = 1; i <= columnCount; i++) {
                // 通过下标获取字段名，下标从1开始
                String columnLabel = metaData.getColumnLabel(i);
                // 通过下标获取字段值，下标从1开始
                Object object = resultSet.getObject(i);
                // TODO: 5. 通过反射获取类的属性，传入对应的字段名，完成数据-》实体类的映射
                Field declaredField = clazz.getDeclaredField(columnLabel);
                // TODO: 6. 打破private修饰限制，可以设置属性
                declaredField.setAccessible(true);
                // param1：要赋值的对象，如果是静态属性，可以为null。param2：具体的属性值
                declaredField.set(t, object);
            }
            // TODO: 7. 将一行数据转为一个实例对象后，保存到List
            mapList.add(t);
        }

        // 关闭资源
        resultSet.close();
        preparedStatement.close();
        /**
         * 需要考虑是否开启了事务
         * 1. 如果开启了事务，肯定关闭了自动提交，由业务层Service统一处理连接，DAO层忽视
         * 2. 如果没有开启事务，自动提交仍为true，则直接调用工具类释放连接。
         */
        if (connection.getAutoCommit()) {
            // 没有开启事务，可以关闭，调用工具类释放连接
            JdbcUtils.freeConnection();
        }

        return mapList;
    }

}
